Skip to content

Conversation

@zzaekkii
Copy link
Member

close #4

☑️ 완료 태스크

  • 기존 계획
  • 각 크레이트별 디렉토리 구조 생성
  • domain 크레이트 내 도메인별 모듈 스켈레톤 정의 (model / repository Trait)
  • infra 내부 디렉토리 구조 및 SeaORM entity 관리 위치 확정
  • 어쩌다보니 더 나가버린 진도
  • domain 크레이트: todo 도메인 추가 및 repository 트레잇 정의 + 단위 테스트
  • domain 크레이트: user, social account 도메인 추가 및 repository 트레잇 정의
  • infra 크레이트: todo 엔티티 추가, 매퍼, repository 트레잇 구현
  • infra 크레이트: user, social account 엔티티 추가, 매퍼, repository 트레잇 구현
  • 전체 에러 구조 통일 및 common 크레이트에서 정의한 공통 ErrorCode 트레잇 구현

🔎 PR 내용

기존에 계획했던 건 분명히 패키지 구조 세팅과 스켈레톤 코드 작성까지였는데,
정신을 차려 보니 어느새 domain 크레이트와 infra 크레이트 작업을 모두 마쳐버린 상태였습니다 😮
눈에 보이는 에러랑 개선 사항을 자꾸 고치고, 추가하다보니 한 작업만 하는 게 어렵더군요.

  • 기존 계획: 💀
  • 현재 작업: 🙋‍♂️

스켈레톤이 아니고 블러드랑 미트까지 다 붙은 생명체가 탄생한 것이죠.
팀 프로젝트였다면 양이 너무 많아 백엔드 팀원에게 끔찍한 리뷰였을텐데, 개인 프로젝트니 반성만 하겠습니다.

남은 api 크레이트 작업인 API 기능 구현마저 여기서 하는 건 적합하지 않다고 판단해,
정신차린 김에 지금까지의 작업을 반영하고, 다음 이슈에서 API 기능 구현 작업 착수하겠습니다.

📌 크레이트 및 모듈 구조 정리

현재 프로젝트는 api / domain / infra / common 으로 구성된 멀티 크레이트 기반 아키텍처입니다.
각 계층(크레이트로 구분)이 맡아야 할 책임과 역할을 명확히 구분하기 위해 크레이트 및 모듈 구조를 정리했습니다.

api 크레이트

  • 서비스의 진입점(main.rs) 포함
  • 글로벌 라우팅 및 각 도메인별 routes / handlers 구성
  • 요청/응답 DTO 및 API 응답 구조 정의 (이후 API 구현 작업에서 추가 예정)

domain 크레이트

  • 순수 비즈니스 영역
  • 각 도메인별 모델 (Entity, Value Object)
  • Repository Trait 정의 (DB 접근 추상화)
  • 도메인 비즈니스에서 발생할 수 있는 에러 정의
  • common 크레이트를 제외한 외부 크레이트에 의존 x

infra 크레이트

  • DB 접근 계층
  • SeaORM 기반 엔티티 자동 생성 및 관리
  • 각 도메인별 Repository Trait 구현
  • entity ↔ domain 변환 로직 정의 (매퍼)

common 크레이트 (그대로 냅둠)

  • 전역 에러 구조, 상수, 응답 코드 제공

이것만으로는 전체 구조가 감이 잘 오지 않을 수 있는데,
아래에서 크레이트별 추가 작업 내용을 설명하면서 전체 구조를 보여드리겠습니다.

📝domain 크레이트 작업

image

1. 도메인 모델 정의 - todo, todo_item, user, social_account

image

2. repository trait 정의 - todo, user, social_account

image

Java/Spring 프로젝트에서 객체지향을 최대한 살려 유지보수와 확장에 유리하도록,
repository를 인터페이스로 두고, 그걸 jpa를 쓰든 해서 impl 구현체를 만드는 경우가 흔한데요.

마찬가지로 Rust에서도 trait을 정의해두고 impl로 구현하는 방식을 사용합니다.
또한 내부에 async fn으로 비동기 함수로 정의된 걸 볼 수 있는데요.

    async fn insert(&self, user: &mut User) -> Result<User, anyhow::Error>;
    async fn update(&self, user: &User) -> Result<User, anyhow::Error>;
    async fn delete(&self, id: i64) -> Result<(), anyhow::Error>;

코드를 보다 보니, 잉? 함수에 모두 async가 붙어있는 걸 볼 수 있을텐데요.
Rust의 IO는 기본적으로 비동기 기반이라, 동시성 확보를 위해 async/await를 주로 사용합니다.
스레드를 많이 쓰는 Spring과 달리 Rust는 단일 스레드에서도 수천 개 비동기 작업을 처리할 수 있기 때문이죠.
Rust async는 기본적으로 경량 task(future)를 tokio 런타임이 관리하며 스케줄링하는 구조입니다 (≒코루틴인거죠)


Java에선 이걸 극복하기 위해 virtual thread를 도입했죠? 이런 이유때문에 `repository`에서도 자연스럽게 async 함수로 정의한 것입니다.

이전에 학교에서도 Rust 비동기 주제를 발표했었는데,
대부분 Java에 익숙한데다 Rust에 대해 잘 모르니 Java와 비교하며 설명했을 때 가장 이해하기 쉬워하더라고요.

image

또한 Spring 프로젝트에서는 편리한 ORM, Dirty Check, 엔티티 영속성 관리를 위해 JPA를 많이들 사용하죠.
Rust에선 그런 추상적이고 자동화된 기능이 없습니다. SeaORM 크레이트를 썼지만 이건 동일합니다.

JPA에서는 dirty check를 기반으로 save()로 통합해서 사용하는 게 자연스럽지만,
Rust + Axum + SeaORM 환경에서는 그런 기능이 제공되지 않아 작업을 명시적으로 분리해야 합니다.
insert(), update(), delete()와 같이 말이죠.

또한 Rust엔 exception이 없기 때문에, 에러를 Result<> 반환 타입으로 명시적으로 전달해야 합니다.
그래서 모든 함수의 반환 타입이 Result<> 구조인 것이죠.

3. 에러 코드 정의 및 enum으로 관리 - todo_error_code, user_error_code

image

4. 도메인 서비스 유닛 테스트 - todo, todo_item

이야 이건, 솔직히 우테코 프리코스 1-3주차 과제를 하면서 유닛 테스트를 공부하고 추가해봐서인지,
뭘 테스트해야 하고, 어떻게 추가해야 할 지 바로 생각이 들더라고요.
여태껏 프로젝트를 진행할 때는 테스트를 어떻게 해야할 지 감도 안 잡혔는데, 아주 놀랐습니다.

아무튼, Rust에서는 Java/Spring과 다르게 테스트용 메인 클래스를 두지 않습니다.
Rust는 안정성을 중시하기로 유명하지 않습니까? 해당 모듈 내부 구현 검증을 위해 그 모듈 내 테스트를 둡니다.

모듈 안에 #[cfg(test)]를 이용해 바로 테스트 코드를 넣는 게 일반적인데요.
Rust 언어 자체적으로 cargo test를 이용해 모듈 단위로 빠르게 단위 테스트하는 걸 지향해서 그렇습니다.

#[cfg()]는 Rust 언어 자체적으로 지원하는 조건부 컴파일 기능인데,
#[cfg(test)]가 붙으면 테스트 빌드에선 테스트 모듈이 포함되고, 릴리즈 빌드에선 제거됩니다.

image

그렇게 상황을 나눠 각 유닛 테스트를 추가하고 돌렸을 때, 모두 초록불이 나오니 마음이 편안해지더군요 😊👍

지금 고민 중인 부분은 todo 는 하루에 최대 3개까지 추가 가능한 개별 할 일 todo_item의 범위까지 묶었는데요.
개별 할 일은 특정 일자의 할 일 목록에 포함되는 일이기 때문이죠.

이걸 도메인 주도 설계에선 같은 어그리거트 루트로 묶는다고들 하지 않습니까?
(아직 도메인 주도 설계 첫걸음 책 한 권 부분독 지식만 갖춘 상태라 정확치 않을 수 있음)

그와 마찬가지로 지금은 usersocial_account에 대해 repository를 각각 뒀는데,
소셜 로그인도 결국 유저 활동의 일환이니 묶어야 하나 고민 중입니다.

다만 아직 API 기능 구현도 완성하지 못 한 상황에서 퀄리티를 높이기 위해 고민하기보다
우선 프로토타입까지 끝낸 뒤에 리팩토링하면서 다듬으려고 일단 냅뒀습니다.

💾 infra 크레이트 작업

image

크레이트 내부 모듈 구조는 이런 상태인데요.
엔티티 → 매퍼 → repository trait 구현 순서로 설명하겠습니다.

1. 엔티티 추가 - todo, todo_item, user, social_account

image

SeaORM은 Rust 생태계에서 가장 많이 쓰이는 비동기 ORM인데요.
테이블 매핑, 쿼리, 관계 정의 기능이 제공돼서 DB 접근이 타입 safe 하게 되죠.

그걸 위해 필요한 것들을 엔티티 내부에 정의하고, 설정해줬습니다.

2. 도메인 모델 <-> DB 엔티티 간 변환을 위한 매퍼 구현 - todo_mapper, user_mapper, social_account_mapper

image

domain 크레이트는 서비스의 핵심 비즈니스를 담는 코어로 외부 의존이 없는 순수 Rust로 이뤄진 계층입니다.
외부 기술(ORM, DB 스키마)에 영향을 받지 않는 순수 비즈니스 로직이니, SeaORM Model/ActiveModel을 직접 사용할 수 없죠..
그래서 domain 크레이트 내 도메인 모델과 infra 크레이트 내 DB 엔티티가 분리되어야 하는 것이죠.
infra 계층에서 DB 엔티티 ↔ 도메인 모델 간 변환을 담당하는 매퍼를 이런 이유에서 추가해준 것입니다.

멀티 크레이트 구조에서 계층 흐름 역시 유지돼야 합니다.

infra → domain 방향으로만 의존하도록 설계했으니,
infra에서 DB 엔티티를 읽고 domain 모델로 변환하는 걸 infra 계층의 매퍼가 수행하는 것이죠.

image

신규 데이터의 id는 DB 자동 증가되어 정해지므로 도메인 객체에 id를 세팅할 수 없으므로 Option<> 타입이죠.
DB에서 데이터를 꺼내올 땐 id가 존재하니 이를 세팅해주려면 안티패턴인 setter 대신 builder를 이용하기로 했습니다.
(그래서 도메인 객체에 유도 트레잇으로 Builder를 넣어둔 것이죠)

매퍼가 엔티티 ActiveModel로 전환 시에는 NotSet 처리하고, DB에서 읽은 id를 builder를 사용해 세팅해줍니다.

3. domain 크레이트에서 정의해둔 repository trait 구현 - todo_repository_iml, user_repository_impl

image

🔥 Spring과 달리 Rust Axum에서는 DI/빈 관리가 자동이 아닙니다🔥
Axum 프레임워크만 그런 게 아니라, actix, rocket도 마찬가지로 Rust 웹 생태계에선 그렇습니다.
사실 빈이니 DI니, Spring에 너무 익숙해서 그렇지 Spring 프레임워크의 특성이죠?

Java/Spring은 스프링 컨테이너가 DB 커넥션 풀, 트랜잭션, Repository 빈을 모두 자동 관리해주지만
Rust에서는 이런 추상화가 없어서 직접 구조체를 만들고 그 안에 DB 커넥션을 직접 주입해줘야 합니다.

즉, UserRepositoryImpl { db } 자체가 Rust식 Repository 빈 역할을 수행하는 거죠.
api 크레이트에서도 다음 pr에서 설명하겠지만 그 이유로 State 만들어서 공유하고 그러는 거죠.

또한, SQL 쿼리를 직접 작성하지 않고 SeaORM에서 제공하는 함수로 타입 safe하게 구현했습니다.


📬 api 크레이트 구조

image

보이는 구조와 같이 todo, user 모듈 구조만 잡아두고, 더미 파일만 생성해둔 상태입니다.
각 모듈에 라우트, 핸들러가 들어있고, 이건 API를 구현하면서 채워나갈 예정입니다.

카카오톡 소셜 로그인 구현은 오늘 내에 가능할지 확언하기 어렵군요.

최초 계획으로는 소셜 로그인 구현까지 마치고, 프론트와 API 통신까지 마치는 거였는데,
처음 쓰는 프레임워크와 언어로 공부하면서 개발을 진행해나가는 게 시간이 꽤 걸리는 일이더라고요.


최초 계획: 클라우드 배포까지 끝내자!
→ 로컬에서라도 동작하게 하자!
→ 백엔드만이라도 다 끝내자!
→ 로그인빼고 API만이라도 다 만들자!
→ ...

아무튼 최근 2주동안 밤 새워가면서 계속 혼자 공부 & 작업 & 공부 & 작업하고 있는데,
마감까지 최선을 다해보겠습니다.

@zzaekkii zzaekkii self-assigned this Nov 24, 2025
@zzaekkii zzaekkii added backend 백엔드 이슈 feature 기능 이슈 test 테스트 이슈 labels Nov 24, 2025
@zzaekkii zzaekkii linked an issue Nov 24, 2025 that may be closed by this pull request
3 tasks
@zzaekkii zzaekkii added the chore 설정 및 기타 이슈 label Nov 24, 2025
@zzaekkii zzaekkii merged commit 8962dec into develop Nov 24, 2025
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend 백엔드 이슈 chore 설정 및 기타 이슈 feature 기능 이슈 test 테스트 이슈

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[chore/package-structure] 크레이트 및 모듈 구조 분리

2 participants